/*
 * Copyright (C) 2013 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { kotlinRequire } from "./utils/Utils"
import { List } from "@kit.ArkTS"
import { Call } from "./Call"
import { AsyncCall, RealCall } from "./connection/RealCall"
import { Runnable } from "./concurrent/Runnable"
import { AssertionError } from "./Error/error"

/**
 * Policy on when async requests are executed.
 *
 * Each dispatcher uses an [ExecutorService] to run calls numberernally. If you supply your own
 * executor, it should be able to run [the configured maximum][maxRequests] number of calls
 * concurrently.
 */
export class Dispatcher{
  /**
   * The maximum number of requests to execute concurrently. Above this requests queue in memory,
   * waiting for the running calls to complete.
   *
   * If more than [maxRequests] requests are in flight when this is invoked, those requests will
   * remain in flight.
   */
  maxRequests = 64
  get getMaxRequest(){
    return this.maxRequests
  }
  set setMaxRequest(maxRequests) {
    kotlinRequire(maxRequests >= 1, `max < 1: ${maxRequests}`)
    // synchronized(this) {
    this.maxRequests = maxRequests
    // }
    this.promoteAndExecute()
  }

  /**
   * The maximum number of requests for each host to execute concurrently. This limits requests by
   * the URL's host name. Note that concurrent requests to a single IP address may still exceed this
   * limit: multiple hostnames may share an IP address or be routed through the same HTTP proxy.
   *
   * If more than [maxRequestsPerHost] requests are in flight when this is invoked, those requests
   * will remain in flight.
   *
   * WebSocket connections to hosts **do not** count against this limit.
   */
  maxRequestsPerHost = 5
  get getMaxRequestsPerHost() {
    return this.maxRequestsPerHost
  }
  set setMaxRequestsPerHost(maxRequestsPerHost) {
    kotlinRequire(maxRequestsPerHost >= 1, `max < 1: ${maxRequestsPerHost}`)
    // synchronized(this) {
    this.maxRequestsPerHost = maxRequestsPerHost
    // }
    this.promoteAndExecute()
  }

  /**
   * A callback to be invoked each time the dispatcher becomes idle (when the number of running
   * calls returns to zero).
   *
   * Note: The time at which a [call][Call] is considered idle is different depending on whether it
   * was run [asynchronously][Call.enqueue] or [synchronously][Call.execute]. Asynchronous calls
   * become idle after the [onResponse][Callback.onResponse] or [onFailure][Callback.onFailure]
   * callback has returned. Synchronous calls become idle once [execute()][Call.execute] returns.
   * This means that if you are doing synchronous calls the network layer will not truly be idle
   * until every returned [Response] has been closed.
   */
  // @set:Synchronized
  // @get:Synchronized
  idleCallback?: Runnable | null = null

  // private executorServiceOrNull?: ExecutorService | null = null

  // @get:Synchronized
  // @get:JvmName("executorService")
  // ExecutorService :: 线程池实例 暂不实现
  // executorService: ExecutorService
  // get getExecutorService() {
  //   if (this.executorServiceOrNull === null) {
  //     this.executorServiceOrNull = ThreadPoolExecutor(0, number.MAX_letUE, 60, TimeUnit.SECONDS,
  //       SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
  //   }
  //   return this.executorServiceOrNull
  // }

  /** Ready async calls in the order they'll be run. */
  private readyAsyncCalls = Array<AsyncCall>()

  /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
  private runningAsyncCalls = Array<AsyncCall>()

  /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
  private runningSyncCalls = Array<RealCall>()

  // constructor(executorService: ExecutorService){
  //   this.executorServiceOrNull = executorService
  // }

  enqueue(call: AsyncCall) {
    // synchronized(this) {
      this.readyAsyncCalls.push(call)

      // Mutate the AsyncCall so that it shares the Atomicnumbereger of an existing running call to
      // the same host.
      if (!call.call.forWebSocket) {
        let existingCall = this.findExistingCallWithHost(call.host)
        if (existingCall !== null && existingCall !== undefined) call.reuseCallsPerHostFrom(existingCall)
      }
    // }
    this.promoteAndExecute()
  }

  private findExistingCallWithHost(host: string): AsyncCall | null{
    for (const existingCall of this.runningAsyncCalls) {
      if (existingCall.host === host) return existingCall
    }
    for (const existingCall of this.readyAsyncCalls) {
      if (existingCall.host === host) return existingCall
    }
    return null
  }

  /**
   * Cancel all calls currently enqueued or executing. Includes calls executed both
   * [synchronously][Call.execute] and [asynchronously][Call.enqueue].
   */
  // @Synchronized
  async cancelAll() {
    for (const call of this.readyAsyncCalls) {
      await call.call.cancel()
    }
    for (const call of this.runningAsyncCalls) {
      await call.call.cancel()
    }
    for (const call of this.runningSyncCalls) {
      await call.cancel()
    }
  }

  /**
   * Promotes eligible calls from [readyAsyncCalls] to [runningAsyncCalls] and runs them on the
   * executor service. Must not be called with synchronization because executing calls can call
   * into user code.
   *
   * @return true if the dispatcher is currently running calls.
   */
  private promoteAndExecute(): boolean {
    // this.assertThreadDoesntHoldLock()

    const executableCalls = Array<AsyncCall>()
    let isRunning: boolean
    // synchronized(this) {
    let i = 0
    while (i < this.readyAsyncCalls.length) {
      const asyncCall = this.readyAsyncCalls[i]

      if (this.runningAsyncCalls.length >= this.maxRequests) break // Max capacity.
      if (asyncCall.callsPerHost >= this.maxRequestsPerHost) {
        i++
        continue
      } // Host max capacity.

      this.readyAsyncCalls.splice(i, 1)
      asyncCall.callsPerHost += 1
      executableCalls.push(asyncCall)
      this.runningAsyncCalls.push(asyncCall)
    }
    isRunning = this.runningCallsCount() > 0
    // }
    
    for (let i = 0; i < executableCalls.length; i++) {
      const asyncCall = executableCalls[i]
      asyncCall.executeOn()
    }
    return isRunning
  }

  /** Used by [Call.execute] to signal it is in-flight. */
  // @Synchronized
  executed(call: RealCall) {
    this.runningSyncCalls.push(call)
  }

  /** Used by [AsyncCall.run] to signal completion. */
  finishedAsync(call: AsyncCall): void {
    call.callsPerHost -= 1
    this.finished_(this.runningAsyncCalls, call)
  }

  /** Used by [Call.execute] to signal completion. */
  finished(call: RealCall): void {
    this.finished_(this.runningSyncCalls, call)
  }

  private finished_<T>(calls: Array<T>, call: T): void {
    let idleCallback: Runnable | null | undefined
    // synchronized(this) {
    const index = calls.indexOf(call)
    if (index === -1) throw new AssertionError("Call wasn't in-flight!")
    calls.splice(index, 1)
    idleCallback = this.idleCallback
    // }
    
    const isRunning = this.promoteAndExecute()
    if (!isRunning && idleCallback !== null && idleCallback !== undefined) {
      idleCallback.run()
    }
  }

  /** Returns a snapshot of the calls currently awaiting execution. */
  // @Synchronized
  queuedCalls(): ReadonlyArray<Call> {
    const calls: Call[] = []
    for (const readyAsyncCall of this.readyAsyncCalls) {
      calls.push(readyAsyncCall.call)
    }
    return calls
  }

  /** Returns a snapshot of the calls currently being executed. */
  // @Synchronized
  runningCalls(): ReadonlyArray<Call> {
    const calls: Call[] = []
    for (const runningSyncCall of this.runningSyncCalls) {
      calls.push(runningSyncCall)
    }
    for (const runningAsyncCall of this.runningAsyncCalls) {
      calls.push(runningAsyncCall.call)
    }
    return calls
  }

  // @Synchronized
  queuedCallsCount(): number {
    return this.readyAsyncCalls.length
  }

  // @Synchronized
  runningCallsCount(): number {
    return this.runningAsyncCalls.length + this.runningSyncCalls.length
  }

}
